'use strict';

import chai from 'chai';
import 'chai/register-should';
import util from 'util';
import sinon from 'sinon';
import log from 'npmlog';
import { Common } from '../../src/lib/common';
import helpers from './helpers';
import { FiatRateService } from '../../src/lib/fiatrateservice';

log.debug = log.verbose;
log.level = 'info';
const should = chai.should();
const { Defaults, Constants } = Common;

describe('Fiat rate service', function() {
  this.timeout(5000);
  let service;
  let request;

  before(async function() {
    await helpers.before();
  });

  after(async function() {
    await helpers.after();
  });

  beforeEach(async function() {
    await helpers.beforeEach();
    service = new FiatRateService();
    request = sinon.stub();
    request.get = sinon.stub();
    await util.promisify(service.init).call(service, {
      storage: helpers.getStorage(),
      request: request
    });
    await util.promisify(service.startCron).call(service, {});
  });

  describe('#getRate', function() {
    it('should get current rate', function(done) {
      service.storage.storeFiatRate(
        'bch',
        [
          {
            code: 'USD',
            value: 123.45
          }
        ],
        function(err) {
          should.not.exist(err);
          service.getRate(
            {
              coin: 'bch',
              code: 'USD'
            },
            function(err, res) {
              should.not.exist(err);
              res.rate.should.equal(123.45);
              done();
            }
          );
        }
      );
    });
    it('should get current rate for different currency', function(done) {
      service.storage.storeFiatRate(
        'btc',
        [
          {
            code: 'USD',
            value: 123.45
          }
        ],
        function(err) {
          should.not.exist(err);
          service.storage.storeFiatRate(
            'btc',
            [
              {
                code: 'EUR',
                value: 345.67
              }
            ],
            function(err) {
              should.not.exist(err);
              service.getRate(
                {
                  code: 'EUR'
                },
                function(err, res) {
                  should.not.exist(err);
                  res.rate.should.equal(345.67);
                  done();
                }
              );
            }
          );
        }
      );
    });

    it('should get current rate for different coin', function(done) {
      service.storage.storeFiatRate(
        'btc',
        [
          {
            code: 'USD',
            value: 100.0
          }
        ],
        function(err) {
          should.not.exist(err);
          service.storage.storeFiatRate(
            'bch',
            [
              {
                code: 'USD',
                value: 200.0
              }
            ],
            function(err) {
              should.not.exist(err);
              service.getRate(
                {
                  code: 'USD'
                },
                function(err, res) {
                  should.not.exist(err);
                  res.rate.should.equal(100.0, 'Should use default coin');
                  service.getRate(
                    {
                      code: 'USD',
                      coin: 'bch'
                    },
                    function(err, res) {
                      should.not.exist(err);
                      res.rate.should.equal(200.0);
                      done();
                    }
                  );
                }
              );
            }
          );
        }
      );
    });

    it('should get rate for specific ts', function(done) {
      const clock = sinon.useFakeTimers({ toFake: ['Date'] });
      clock.tick(20);
      service.storage.storeFiatRate(
        'btc',
        [
          {
            code: 'USD',
            value: 123.45
          }
        ],
        function(err) {
          should.not.exist(err);
          clock.tick(100);
          service.storage.storeFiatRate(
            'btc',
            [
              {
                code: 'USD',
                value: 345.67
              }
            ],
            function(err) {
              should.not.exist(err);
              service.getRate(
                {
                  code: 'USD',
                  ts: 50
                },
                function(err, res) {
                  should.not.exist(err);
                  res.ts.should.equal(50);
                  res.rate.should.equal(123.45);
                  res.fetchedOn.should.equal(20);
                  clock.restore();
                  done();
                }
              );
            }
          );
        }
      );
    });

    it('should get rates for a series of ts', async function() {
      const clock = sinon.useFakeTimers({ toFake: ['Date'] });
      
      for (const value of [1.0, 2.0, 3.0, 4.0]) {
        clock.tick(100);
        await util.promisify(service.storage.storeFiatRate).call(service.storage,
          'btc',
          [
            {
              code: 'USD',
              value: value
            },
            {
              code: 'EUR',
              value: value
            }
          ],
        );
      }
      const res = await util.promisify(service.getRate).call(service, {
        code: 'USD',
        ts: [50, 100, 199, 500]
      });
      res.length.should.equal(4);

      res[0].ts.should.equal(50);
      should.not.exist(res[0].rate);
      should.not.exist(res[0].fetchedOn);

      res[1].ts.should.equal(100);
      res[1].rate.should.equal(1.0);
      res[1].fetchedOn.should.equal(100);

      res[2].ts.should.equal(199);
      res[2].rate.should.equal(1.0);
      res[2].fetchedOn.should.equal(100);

      res[3].ts.should.equal(500);
      res[3].rate.should.equal(4.0);
      res[3].fetchedOn.should.equal(400);

      clock.restore();
    });

    it('should get historical rates from ts to now', async function() {
      const coins = ['btc', 'bch', 'eth', 'matic', 'xrp', 'doge', 'ltc'];
      const clock = sinon.useFakeTimers({ toFake: ['Date'] });
      for (const value of [1.0, 2.0, 3.0, 4.0, 5.0]) {
        clock.tick(100);
        for (const coin of coins) {
          await util.promisify(service.storage.storeFiatRate).call(service.storage,
            coin,
            [
              {
                code: 'USD',
                value: value
              },
              {
                code: 'EUR',
                value: value
              }
            ]
          );
        }
      }
      const res = await util.promisify(service.getHistoricalRates).call(service, {
        code: 'USD',
        ts: 100
      });
      should.exist(res);

      for (const coin of coins) {
        res[coin].length.should.equal(5);

        res[coin][4].ts.should.equal(100);
        res[coin][4].rate.should.equal(1.0);

        res[coin][3].ts.should.equal(200);
        res[coin][3].rate.should.equal(2.0);

        res[coin][2].ts.should.equal(300);
        res[coin][2].rate.should.equal(3.0);

        res[coin][1].ts.should.equal(400);
        res[coin][1].rate.should.equal(4.0);

        res[coin][0].ts.should.equal(500);
        res[coin][0].rate.should.equal(5.0);
      }
      clock.restore();
    });

    it('should not throw if missing historical rates for a coin', async function() {
      const clock = sinon.useFakeTimers({ toFake: ['Date'] });
      for (const value of [1.0, 2.0, 3.0, 4.0]) {
        clock.tick(100);
        await util.promisify(service.storage.storeFiatRate).call(service.storage,
          'btc',
          [
            {
              code: 'USD',
              value: value
            },
            {
              code: 'EUR',
              value: value
            }
          ]
        );
      }
      const res = await util.promisify(service.getHistoricalRates).call(service, {
        code: 'USD',
        ts: 100
      });
      should.exist(res);
      res['btc'].length.should.equal(4);
      should.not.exist(res['bch']);
      should.not.exist(res['eth']);
      should.not.exist(res['matic']);
      should.not.exist(res['xrp']);
      should.not.exist(res['doge']);
      should.not.exist(res['ltc']);

      res['btc'][3].ts.should.equal(100);
      res['btc'][3].rate.should.equal(1.0);

      res['btc'][2].ts.should.equal(200);
      res['btc'][2].rate.should.equal(2.0);

      res['btc'][1].ts.should.equal(300);
      res['btc'][1].rate.should.equal(3.0);

      res['btc'][0].ts.should.equal(400);
      res['btc'][0].rate.should.equal(4.0);
      clock.restore();
    });

    it('should return current rates if missing opts.ts when fetching historical rates', async function() {
      const coins = ['btc', 'bch', 'eth', 'xrp', 'doge', 'ltc'];
      const clock = sinon.useFakeTimers({ toFake: ['Date'] });
      for (const value of [1.0, 2.0, 3.0, 4.0]) {
        clock.tick(11 * 60 * 1000);
        for (const coin of coins) {
          await util.promisify(service.storage.storeFiatRate).call(service.storage,
            coin,
            [
              {
                code: 'USD',
                value: value
              },
              {
                code: 'EUR',
                value: value
              }
            ]
          );
        }
      }
      
      const res = await util.promisify(service.getHistoricalRates).call(service, {
        code: 'USD'
      });
      for (const coin of coins) {
        res[coin].length.should.equal(1);
        res[coin][0].ts.should.equal(2640000);
        res[coin][0].rate.should.equal(4.0);
      }
      clock.restore();
    });

    it('should not get rate older than 2hs', function(done) {
      const clock = sinon.useFakeTimers({ toFake: ['Date'] });
      service.storage.storeFiatRate(
        'btc',
        [
          {
            code: 'USD',
            value: 123.45
          }
        ],
        function(err) {
          should.not.exist(err);
          clock.tick(24 * 3600 * 1000); // Some time in the future
          service.getRate(
            {
              ts: 2 * 3600 * 1000 - 1, // almost 2 hours
              code: 'USD'
            },
            function(err, res) {
              should.not.exist(err);
              res.rate.should.equal(123.45);
              res.fetchedOn.should.equal(0);
              service.getRate(
                {
                  ts: 2 * 3600 * 1000 + 1, // just past 2 hours
                  code: 'USD'
                },
                function(err, res) {
                  should.not.exist(err);
                  should.not.exist(res.rate);
                  clock.restore();
                  done();
                }
              );
            }
          );
        }
      );
    });
  });

  describe('#fetch', function() {
    it('should fetch rates from all coins', function(done) {
      const clock = sinon.useFakeTimers({ now: 100, toFake: ['Date'] });
      const btc = [
        {
          code: 'USD',
          rate: 123.45
        },
        {
          code: 'EUR',
          rate: 234.56
        }
      ];
      const bch = [
        {
          code: 'USD',
          rate: 120
        },
        {
          code: 'EUR',
          rate: 120
        }
      ];
      const eth = [
        {
          code: 'USD',
          rate: 121
        },
        {
          code: 'EUR',
          rate: 121
        }
      ];
      const matic = [
        {
          code: 'USD',
          rate: 1.19
        },
        {
          code: 'EUR',
          rate: 1.19
        }
      ];
      const xrp = [
        {
          code: 'USD',
          rate: 0.222222
        },
        {
          code: 'EUR',
          rate: 0.211111
        }
      ];
      const doge = [
        {
          code: 'USD',
          rate: 0.05
        },
        {
          code: 'EUR',
          rate: 0.04
        }
      ];
      const ltc = [
        {
          code: 'USD',
          rate: 150
        },
        {
          code: 'EUR',
          rate: 170
        }
      ];
      const shib = [
        {
          code: 'USD',
          rate: 0.00003678
        },
        {
          code: 'EUR',
          rate: 0.00003276
        }
      ];
      const ape = [
        {
          code: 'USD',
          rate: 6.66
        },
        {
          code: 'EUR',
          rate: 6.64
        }
      ];
      const sol = [
        {
          code: 'USD',
          rate: 150
        },
        {
          code: 'EUR',
          rate: 156
        }
      ];

      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/BTC?p=bws',
          json: true
        })
        .yields(null, null, btc);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/BCH?p=bws',
          json: true
        })
        .yields(null, null, bch);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/ETH?p=bws',
          json: true
        })
        .yields(null, null, eth);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/MATIC?p=bws',
          json: true
        })
        .yields(null, null, matic);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/XRP?p=bws',
          json: true
        })
        .yields(null, null, xrp);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/DOGE?p=bws',
          json: true
        })
        .yields(null, null, doge);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/LTC?p=bws',
          json: true
        })
        .yields(null, null, ltc);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/SHIB?p=bws',
          json: true
        })
        .yields(null, null, shib);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/APE?p=bws',
          json: true
        })
        .yields(null, null, ape);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/SOL?p=bws',
          json: true
        })
        .yields(null, null, sol);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/WBTC?p=bws',
          json: true
        })
        .yields(null, null, btc);
      request.get
        .withArgs({
          url: 'https://bitpay.com/api/rates/WETH?p=bws',
          json: true
        })
        .yields(null, null, eth);

      service._fetch(function(err) {
        should.not.exist(err);
        service.getRate(
          {
            code: 'USD'
          },
          function(err, res) {
            should.not.exist(err);
            res.fetchedOn.should.equal(100);
            res.rate.should.equal(123.45);
            service.getRate(
              {
                code: 'USD',
                coin: 'bch'
              },
              function(err, res) {
                should.not.exist(err);
                res.fetchedOn.should.equal(100);
                res.rate.should.equal(120.0);
                service.getRate(
                  {
                    code: 'USD',
                    coin: 'eth'
                  },
                  function(err, res) {
                    should.not.exist(err);
                    res.fetchedOn.should.equal(100);
                    res.rate.should.equal(121);
                    service.getRate(
                      {
                        code: 'USD',
                        coin: 'matic'
                      },
                      function(err, res) {
                        should.not.exist(err);
                        res.fetchedOn.should.equal(100);
                        res.rate.should.equal(1.19);
                        service.getRate(
                          {
                            code: 'USD',
                            coin: 'xrp'
                          },
                          function(err, res) {
                            should.not.exist(err);
                            res.fetchedOn.should.equal(100);
                            res.rate.should.equal(0.222222);
                            service.getRate(
                              {
                                code: 'USD',
                                coin: 'doge'
                              },
                              function(err, res) {
                                should.not.exist(err);
                                res.fetchedOn.should.equal(100);
                                res.rate.should.equal(0.05);
                                service.getRate(
                                  {
                                    code: 'USD',
                                    coin: 'ltc'
                                  },
                                  function(err, res) {
                                    should.not.exist(err);
                                    res.fetchedOn.should.equal(100);
                                    res.rate.should.equal(150);
                                    service.getRate(
                                      {
                                        code: 'EUR'
                                      },
                                      function(err, res) {
                                        should.not.exist(err);
                                        res.fetchedOn.should.equal(100);
                                        res.rate.should.equal(234.56);
                                        service.getRate(
                                          {
                                            code: 'USD',
                                            coin: 'shib'
                                          },
                                          function(err, res) {
                                            should.not.exist(err);
                                            res.fetchedOn.should.equal(100);
                                            res.rate.should.equal(0.00003678);
                                            service.getRate(
                                              {
                                                code: 'USD',
                                                coin: 'ape'
                                              },
                                              function(err, res) {
                                                should.not.exist(err);
                                                res.fetchedOn.should.equal(100);
                                                res.rate.should.equal(6.66);
                                                service.getRate(
                                                  {
                                                    code: 'USD',
                                                    coin: 'usdc'
                                                  },
                                                  function(err, res) {
                                                    should.not.exist(err);
                                                    res.fetchedOn.should.equal(100);
                                                    res.rate.should.equal(1);
                                                    service.getRate(
                                                      {
                                                        code: 'USD',
                                                        coin: 'weth'
                                                      },
                                                      function(err, res) {
                                                        should.not.exist(err);
                                                        res.fetchedOn.should.equal(100);
                                                        res.rate.should.equal(121);
                                                        service.getRate(
                                                          {
                                                            code: 'USD',
                                                            coin: 'sol'
                                                          },
                                                          function(err, res) {
                                                            should.not.exist(err);
                                                            res.fetchedOn.should.equal(100);
                                                            res.rate.should.equal(150);
                                                            clock.restore();
                                                            done();
                                                          }
                                                        );
                                                      }
                                                    );
                                                  }
                                                );
                                              }
                                            );
                                          }
                                        );
                                      }
                                    );
                                  }
                                );
                              }
                            );
                          }
                        );
                      }
                    );
                  }
                );
              }
            );
          }
        );
      });
    });
  });

  describe('#getRates', function() {
    it('should get the rates of each coin in all supported fiat currencies', function(done) {
      service.getRates({}, function(err, res) {
        should.not.exist(err);
        for (const key of Object.keys(res)) {
          res[key].length.should.equal(Defaults.FIAT_CURRENCIES.length);
        }
        done();
      });
    });
    it('should get the rates of all coins supported', function(done) {
      service.getRates({}, function(err, res) {
        should.not.exist(err);
        Object.keys(res).length.should.equal( Object.keys(Constants.BITPAY_SUPPORTED_COINS).length);
        done();
      });
    });
    it('should get rates of all coins in the specified fiat currency if it is supported', function(done) {
      service.getRates(
        {
          code: 'EUR'
        },
        function(err, res) {
          should.not.exist(err);
          Object.keys(res).length.should.equal( Object.keys(Constants.BITPAY_SUPPORTED_COINS).length);
          for (const key of Object.keys(res)) {
            res[key].length.should.equal(1);
          }
          done();
        }
      );
    });
    it('should throw error if the specified fiat currency code is not supported', function(done) {
      service.getRates(
        {
          code: 'AOA'
        },
        function(err) {
          should.exist(err);
          err.should.equal('AOA is not supported');
          done();
        }
      );
    });
    it('should get rate for specific ts', function(done) {
      const clock = sinon.useFakeTimers({ toFake: ['Date'] });
      clock.tick(20);
      service.storage.storeFiatRate(
        'btc',
        [
          {
            code: 'USD',
            value: 123.45
          }
        ],
        function(err) {
          should.not.exist(err);
          clock.tick(100);
          service.storage.storeFiatRate(
            'btc',
            [
              {
                code: 'USD',
                value: 345.67
              }
            ],
            function(err) {
              should.not.exist(err);
              service.getRates(
                {
                  coin: 'btc',
                  code: 'USD',
                  ts: 50
                },
                function(err, res) {
                  should.not.exist(err);
                  for (const key of Object.keys(res)) {
                    res[key][0].ts.should.equal(50);
                  }
                  clock.restore();
                  done();
                }
              );
            }
          );
        }
      );
    });
  });

  describe('#getRatesByCoin', function() {
    const bchRates = [
      { code: 'USD', value: 268.94 },
      { code: 'INR', value: 19680.35 },
      { code: 'EUR', value: 226.37 },
      { code: 'CAD', value: 352.33 },
      { code: 'COP', value: 1026617.26 },
      { code: 'NGN', value: 104201.93 },
      { code: 'GBP', value: 201.62 },
      { code: 'ARS', value: 19900.21 },
      { code: 'AUD', value: 365.8 },
      { code: 'BRL', value: 1456.93 },
      { code: 'JPY', value: 1124900.43 },
      { code: 'NZD', value: 16119.66 }
    ];
    const btcRates = [
      { code: 'USD', value: 44173.54 },
      { code: 'INR', value: 3306411.94 },
      { code: 'GBP', value: 32558.07 },
      { code: 'EUR', value: 38642.15 },
      { code: 'CAD', value: 56070.58 },
      { code: 'COP', value: 173924929.36 },
      { code: 'NGN', value: 18409323.7 },
      { code: 'BRL', value: 233457.17 },
      { code: 'ARS', value: 4678684.28 },
      { code: 'AUD', value: 61503.4 },
      { code: 'JPY', value: 5099004.98 },
      { code: 'NZD', value: 50966068.64 }
    ];
    it('should get rates for all the supported fiat currencies of the specified coin', function(done) {
      service.storage.storeFiatRate('bch', bchRates, function(err) {
        should.not.exist(err);
        service.getRatesByCoin(
          {
            coin: 'bch'
          },
          function(err, res) {
            should.not.exist(err);
            res.length.should.equal(bchRates.length);
            done();
          }
        );
      });
    });
    it('should get rate for the specified coin and currency if they are supported', function(done) {
      service.storage.storeFiatRate('bch', bchRates, function(err) {
        should.not.exist(err);
        service.getRatesByCoin(
          {
            coin: 'bch',
            code: 'EUR'
          },
          function(err, res) {
            should.not.exist(err);
            res[0].rate.should.equal(226.37);
            done();
          }
        );
      });
    });
    it('should throw error if the specified currency code is not supported', function(done) {
      service.storage.storeFiatRate('bch', bchRates, function(err) {
        should.not.exist(err);
        service.getRatesByCoin(
          {
            coin: 'bch',
            code: 'AOA'
          },
          function(err) {
            should.exist(err);
            err.should.equal('AOA is not supported');
            done();
          }
        );
      });
    });
    it('should get fiat rates for a USD stablecoin', function(done) {
      sinon.spy(service, 'getRatesForStablecoin');
      service.storage.storeFiatRate('btc', btcRates, function(err) {
        should.not.exist(err);
        service.getRatesByCoin({ coin: 'gusd' }, function(err, res) {
          should.not.exist(err);
          should.exist(res);
          res
            .reduce((rates, { code, rate }) => ({ ...rates, [code]: rate }), {})
            .should.deep.equal({
              ARS: 105.92,
              AUD: 1.39,
              BRL: 5.29,
              CAD: 1.27,
              COP: 3937.31,
              EUR: 0.87,
              GBP: 0.74,
              INR: 74.85,
              JPY: 115.43,
              NGN: 416.75,
              NZD: 1153.77,
              USD: 1
            });
          service.getRatesForStablecoin
            .calledWith(
              sinon.match({
                code: 'USD',
                ts: sinon.match.number
              })
            )
            .should.equal(true);
          done();
        });
      });
    });
    it('should get rate for specific ts', function(done) {
      const clock = sinon.useFakeTimers({ toFake: ['Date'] });
      clock.tick(20);
      service.storage.storeFiatRate(
        'btc',
        [
          {
            code: 'USD',
            value: 123.45
          }
        ],
        function(err) {
          should.not.exist(err);
          clock.tick(100);
          service.storage.storeFiatRate(
            'btc',
            [
              {
                code: 'USD',
                value: 345.67
              }
            ],
            function(err) {
              should.not.exist(err);
              service.getRatesByCoin(
                {
                  coin: 'btc',
                  code: 'USD',
                  ts: 50
                },
                function(err, res) {
                  should.not.exist(err);
                  res[0].ts.should.equal(50);
                  res[0].rate.should.equal(123.45);
                  res[0].fetchedOn.should.equal(20);
                  clock.restore();
                  done();
                }
              );
            }
          );
        }
      );
    });
  });
});
